package flumsynth;

import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.SourceDataLine;

import tazadum.engine.Signal;
import tazadum.engine.TimeProvider;

/**
 * P
 * <p/>
 * User: druttis Date: 2011-12-21 Time: 22:59
 */
public class PPlayer implements Runnable {
	public byte[] song = new byte[2117632];

	// =================================================================================================================
	// Constants
	// =================================================================================================================
	public static final float SAMPLE_RATE = 44100;
	public static final float TIME_PER_SAMPLE = 1.0f/SAMPLE_RATE;
	public static int SAMPLES_PER_TICK = 5292;
	public static int NUM_TRACKS = 8;
	public static int SEQ_LENGTH = 32;
	public static int PAT_LENGTH = 32;
	public static int SAMPLES_PER_PATTERN = 169344;
	public static final int INSTRUMENT_SIZE = 32;
	public static final int SEQ_BEGIN = 32;
	public static final int PAT_BEGIN = 160;
	public static final int TRACK_SIZE = 16544;
	public static final int BUFFER_SIZE = 21168;
	public static final int DELAY_SIZE = 262144;
	public static final int DELAY_MASK = 262143;
	//
	public static final int OSC1_TYPE = 0;
	public static final int OSC1_PITCH = 1;
	public static final int OSC1_FINE = 2;
	public static final int OSC1_OSC2_PM = 3;
	public static final int OSC1_OSC2_FM = 4;
	public static final int OSC2_TYPE = 5;
	public static final int OSC2_PITCH = 6;
	public static final int OSC2_FINE = 7;
	public static final int OSC_MIX = 8;
	public static final int OSC_SYNC = 9;
	public static final int ENV_ATTACK = 10;
	public static final int ENV_DECAY = 11;
	public static final int ENV_SUSTAIN = 12;
	public static final int ENV_RELEASE = 13;
	public static final int ENV_OSC_FM = 14;
	public static final int ENV_FILTER_FM = 15;
	public static final int LFO_RATE = 16;
	public static final int LFO_OSC_FM = 17;
	public static final int LFO_FILTER_FM = 18;
	public static final int FILTER_TYPE = 19;
	public static final int FILTER_FREQ = 20;
	public static final int FILTER_RES = 21;
	public static final int DIST_GAIN = 22;
	public static final int DIST_THRESHOLD = 23;
	public static final int VOLUME = 24;
	public static final int PAN = 25;
	public static final int DELAY_LEFT_DUR = 26;
	public static final int DELAY_RIGHT_DUR = 27;
	public static final int DELAY_FEEDBACK = 28;
	public static final int DELAY_MIX = 29;
	//
	public static final float PI2 = 6.2831855f;
	//
	private final AtomicBoolean play = new AtomicBoolean();
	private final Semaphore lineWait = new Semaphore(1);

	private final Signal signal;

	private boolean fadeOut = false;
	private float fadeoutTime;
	private float fadeoutDuration;
	private final TimeProvider timeProvider;

	/**
	 * @param signal
	 */
	public PPlayer(final Signal signal, TimeProvider timeProvider) {
		this.signal = signal;
		this.timeProvider = timeProvider;
	}

	public void start() {
		if (play.compareAndSet(false, true)) {
			(new Thread(this)).start();
			lineWait.acquireUninterruptibly();
		}
	}

	public void stop() {
		if (play.compareAndSet(true, false)) {

		}
	}

	public void run() {
		try {
			// =====================================================================================================
			// Setup audio
			// =====================================================================================================
			final AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, SAMPLE_RATE, 16, 2, 4, SAMPLE_RATE, false);
			final SourceDataLine line = AudioSystem.getSourceDataLine(format);
			line.open(format, BUFFER_SIZE);
			line.start();

			// =====================================================================================================
			// Some variables
			// =====================================================================================================
			int idx, cnt, pos;
			float mod;
			final float[] envStage = new float[NUM_TRACKS];
			final float[][] envLevel = new float[NUM_TRACKS][6];
			final float[][] envRate = new float[NUM_TRACKS][5];
			final float[] envOut = new float[NUM_TRACKS];
			final float[] lfoPhase = new float[NUM_TRACKS];
			final float[] osc1Phase = new float[NUM_TRACKS];
			final float[] osc2Phase = new float[NUM_TRACKS];
			final float[] osc1Rate = new float[NUM_TRACKS];
			final float[] osc2Rate = new float[NUM_TRACKS];
			final float[][] filter = new float[NUM_TRACKS][4];
			final float[][] delayL = new float[NUM_TRACKS][DELAY_SIZE];
			final float[][] delayR = new float[NUM_TRACKS][DELAY_SIZE];
			final int[] delay = new int[NUM_TRACKS];
			final byte[] ins = new byte[INSTRUMENT_SIZE];
			final byte[] out = new byte[BUFFER_SIZE];
			// =====================================================================================================
			// Setup envelope
			// =====================================================================================================
			for (int trk = 0, ofs = 0; trk < NUM_TRACKS; trk++, ofs += TRACK_SIZE) {
				for (cnt = 0; cnt < INSTRUMENT_SIZE; cnt++) {
					ins[cnt] = song[ofs + cnt];
				}
				envStage[trk] = 4;
				envLevel[trk][1] = 1f;
				envLevel[trk][2] = ins[ENV_SUSTAIN] / 64f;
				envRate[trk][0] = dur(ins[ENV_ATTACK]);
				envRate[trk][1] = dur(ins[ENV_DECAY]);
				envRate[trk][3] = dur(ins[ENV_RELEASE]);

			}
			lineWait.release();
			// =====================================================================================================
			// Music loop
			// =====================================================================================================
			while (play.get()) {
				for (int seq = 0; seq < SEQ_LENGTH; seq++) {
					if (!play.get()) {
						break;
					}
					for (int pat = 0; pat < PAT_LENGTH; pat++) {
						if (!play.get()) {
							break;
						}
						// =========================================================================================
						// Clear output buffer
						// =========================================================================================
						for (cnt = 0; cnt < BUFFER_SIZE; cnt++) {
							out[cnt] = 0;
						}
						float tickTime = timeProvider.getTime()/1000.0f;
						// =========================================================================================
						// Add tracks to output buffer
						// =========================================================================================
						for (int trk = 0, ofs = 0; trk < NUM_TRACKS; trk++, ofs += TRACK_SIZE) {
							for (cnt = 0; cnt < INSTRUMENT_SIZE; cnt++) {
								ins[cnt] = song[ofs + cnt];
							}
							// =====================================================================================
							// Sequencing
							// =====================================================================================
							idx = song[ofs + PAT_BEGIN + song[ofs + SEQ_BEGIN + seq] * PAT_LENGTH + pat];
							if (idx == 1 && envStage[trk] < 3) {
								envLevel[trk][3] = envOut[trk];
								envStage[trk] = 3;
							} else if (idx > 1) {
								osc1Phase[trk] *= ins[OSC_SYNC];
								osc2Phase[trk] *= ins[OSC_SYNC];
								osc1Rate[trk] = freq(idx, ins[OSC1_PITCH], ins[OSC1_FINE]);
								osc2Rate[trk] = freq(idx, ins[OSC2_PITCH], ins[OSC2_FINE]);
								envLevel[trk][3] = envLevel[trk][2];
								envStage[trk] = 0;
							}
							
							float sampleTime = tickTime;
							// =====================================================================================
							// Generate chunk of samples
							// =====================================================================================
							for (cnt = 0, pos = 0; cnt < SAMPLES_PER_TICK; cnt++, pos += 4) {
								// =================================================================================
								// Generate envelope
								// =================================================================================
								idx = (int) envStage[trk];
								envOut[trk] = envLevel[trk][idx];
								envOut[trk] += (envLevel[trk][idx + 1] - envOut[trk]) * (envStage[trk] - idx);
								envStage[trk] += envRate[trk][idx];

								signal.channel(Signal.CHANNEL_SYNTH_ENV_0 + trk, envOut[trk]);

								// =================================================================================
								// Generate lfo
								// =================================================================================
								final float lfoOut = wave(0, lfoPhase[trk]);
								lfoPhase[trk] += ins[LFO_RATE] / (float) SAMPLES_PER_PATTERN;
								lfoPhase[trk] -= (int) lfoPhase[trk];
								// =================================================================================
								// Generate oscillators
								// =================================================================================
								mod = ins[OSC_MIX] / 128f;
								float outL = wave(ins[OSC1_TYPE], osc1Phase[trk]);
								float temp = wave(ins[OSC2_TYPE], osc2Phase[trk] + outL * ins[OSC1_OSC2_PM] / 64f);
								temp = (outL * (0.5f - mod)) + (temp * (0.5f + mod));
								mod = mod(envOut[trk], ins[ENV_OSC_FM], lfoOut, ins[LFO_OSC_FM]);
								osc1Phase[trk] += osc1Rate[trk] * mod;
								osc1Phase[trk] -= (int) osc1Phase[trk];
								osc2Phase[trk] += osc2Rate[trk] * mod * (float) Math.pow(8f, outL * ins[OSC1_OSC2_FM] / 64f);
								osc2Phase[trk] -= (int) osc2Phase[trk];
								outL = temp;
								// =================================================================================
								// Apply envelope as oscillator VCA
								// =================================================================================
								outL *= envOut[trk];
								// =================================================================================
								// Apply distortion
								// =================================================================================
								outL *= 1f + ins[DIST_GAIN] / 64f;
								mod = ins[DIST_THRESHOLD] / 64f;
								while (outL > mod || outL < -mod) {
									if (outL > mod) {
										outL = mod - (outL - mod);
									}
									if (outL < -mod) {
										outL = (-mod - outL) - mod;
									}
								}
								// =================================================================================
								// Apply filter
								// =================================================================================
								mod = mod(envOut[trk], ins[ENV_FILTER_FM], lfoOut, ins[LFO_FILTER_FM]);
								mod = 1.45f * wave(0, ins[FILTER_FREQ] / 256f * mod);
								filter[trk][0] += mod * filter[trk][2];
								filter[trk][1] = ins[FILTER_RES] / 128f * (outL - filter[trk][2]) - filter[trk][0];
								filter[trk][2] += mod * filter[trk][1];
								filter[trk][3] = filter[trk][0] + filter[trk][1];
								outL = filter[trk][ins[FILTER_TYPE]];
								// =================================================================================
								// Apply volume
								// =================================================================================
								outL *= ins[VOLUME] / 32f;
								
								if (fadeOut && sampleTime > fadeoutTime) {
									final float alpha = 1 - (sampleTime - fadeoutTime) / fadeoutDuration;
									if (alpha < 0) {
										outL = 0;
									} else if (alpha < 1) {
										outL *= alpha;
									}
								}
								
								// =================================================================================
								// Apply panning
								// =================================================================================
								mod = 0.5f + ins[PAN] / 128f;
								float outR = outL * mod;
								outL *= 1f - mod;
								// =================================================================================
								// Apply delay effect
								// =================================================================================
								final float feedback = ins[DELAY_FEEDBACK] / 64f;
								mod = ins[DELAY_MIX] / 64f;
								final float neg = 1f - mod;
								// left
								temp = delayR[trk][(delay[trk] - ins[DELAY_LEFT_DUR] * SAMPLES_PER_TICK) & DELAY_MASK];
								delayL[trk][delay[trk]] = outL + temp * feedback;
								outL = outL * neg + temp * mod;

								// right
								temp = delayL[trk][(delay[trk] - ins[DELAY_RIGHT_DUR] * SAMPLES_PER_TICK) & DELAY_MASK];
								delayR[trk][delay[trk]] = outR + temp * mod;
								outR = outR * neg + temp * mod;
								// advance delay
								delay[trk] = (delay[trk] + 1) & DELAY_MASK;
								// =================================================================================
								// Write to output buffer
								// =================================================================================
								idx = ((out[pos] & 255) | (out[pos + 1] << 8)) + (int) (outL * 32000f);
								out[pos] = (byte) idx;
								out[pos + 1] = (byte) (idx >> 8);
								idx = ((out[pos + 2] & 255) | (out[pos + 3] << 8)) + (int) (outR * 32000f);
								out[pos + 2] = (byte) idx;
								out[pos + 3] = (byte) (idx >> 8);
								
								sampleTime += TIME_PER_SAMPLE;
							}
							// =====================================================================================
							// Sample chunks done
							// =====================================================================================
						}
						// =========================================================================================
						// Tracks done, Audio buffer to speaker
						// =========================================================================================
						idx = 0;
						while (idx < BUFFER_SIZE) {
							idx += line.write(out, idx, Math.min(BUFFER_SIZE - idx, BUFFER_SIZE));
						}
					}
					// =============================================================================================
					// Pattern done
					// =============================================================================================
				}
				// =================================================================================================
				// Sequence done
				// =================================================================================================
			}
			// =====================================================================================================
			// After while loop
			// =====================================================================================================
		} catch (final Exception exc) {
			exc.printStackTrace();
		}
	}

	private static float dur(final int value) {
		return value == 0 ? 1f : 330.75f / (value * value * SAMPLES_PER_TICK);
	}

	private static float freq(final int note, final int pitch, final int fine) {
		return 440f * (float) Math.pow(2f, (note + pitch + fine / 64f - 71f) / 12f) / SAMPLE_RATE;
	}

	private static int rand = 1;

	private static final float[][] WAVE = new float[3][65536];

	static {
		for (int index = 0; index < 65536; index++) {
			WAVE[0][index] = (float) Math.sin(index * PI2 / 65536f);
			WAVE[1][index] = index / 32768f - 1f;
			WAVE[2][index] = index < 32768 ? 1f : -1f;
		}
	}

	private static float wave(final int type, final float phase) {
		switch (type) {
		default:
			return WAVE[type][(int) (phase * 65536f) & 65535];
		case 3:
			rand *= 16807;
			return (rand * 4.6566129e-010f) * 0.99f;
		}
	}

	private static float mod(final float envOut, final int envAmt, final float lfoOut, final int lfoAmt) {
		return (float) Math.pow(8f, envOut * envAmt * 0.01f + lfoOut * lfoAmt * 0.01f);
	}

	public void fadeOut(float time, float duration) {
		fadeOut = true;
		fadeoutTime = time;
		fadeoutDuration = duration;
	}
}
